/*
 * MIT License
 *
 * Copyright (c) 2019 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.commons.digest;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

/**
 * 对称加密
 *
 * @author Mr.css 2016-11-30
 * @version 1.0
 */
public class AES {
    private AES() {
    }

    private static final String CIPHER_AES = "AES";

    /**
     * 密码派生密钥
     */
    private static final String PBKDF2 = "PBKDF2WithHmacSHA256";
    /**
     * CBC 模式
     * <p>
     * CBC模式是一种分组加密模式，它将明文分成固定大小的块（通常是128位，即16字节），
     * 每个明文块在加密前会与前一个密文块进行异或操作，从而引入块与块之间的依赖关系。
     */
    private static final String CIPHER_AES_CBC_PKCS5Padding = "AES/CBC/PKCS5Padding";
    /**
     * GCM 模式
     * <p>
     * AES-GCM（Advanced Encryption Standard - Galois/Counter Mode）
     * 是一种结合了对称加密和消息认证的算法，通常被认为是非常高效且安全的‌；
     * <p>
     * GCM 是 AES 算法的一种操作模式，它结合了计数器模式（CTR）和 Galois 消息认证代码（GMAC）来实现加密和完整性保护‌
     */
    private static final String CIPHER_AES_GCM_NoPadding = "AES/GCM/NoPadding";

    /**
     * PBEKeySpec 迭代次数
     * <p>
     * 指定在密钥派生过程中对密码进行哈希的迭代次数。
     * <p>
     * 增加迭代次数，会增加攻击者破解密码所需的计算时间和资源，可以显著提高密码的安全性。
     */
    private static final int ITERATION_COUNT = 65536;

    /**
     * PBEKeySpec 密钥长度，当前采用 AES-256
     * <p>
     * 指定要派生的密钥的长度（以位为单位）。
     * <p>
     * 这个参数允许用户根据需要生成不同长度的密钥，以适应不同的加密算法和安全要求。
     */
    private static final int KEY_LENGTH = 256;

    /**
     * GCM-TAG 推荐长度
     */
    private static final int GCM_TAG_LENGTH = 128;
    /**
     * GCM-IV 推荐长度
     * <p>
     * bytes (recommended 12 for GCM)
     */
    private static final int GCM_IV_LENGTH = 12;

    /**
     * 加密向量
     *
     * @param iv 向量，长度必须是 16 bytes
     * @return IvParameterSpec
     */
    public static IvParameterSpec genIV(byte[] iv) {
        if (iv.length != 16) {
            throw new IllegalArgumentException("IV length: must be 16 bytes long");
        }
        return new IvParameterSpec(iv);
    }

    /**
     * 获取随机加密向量
     * <p>
     * IV length: must be 16 bytes long
     *
     * @return IV
     */
    public static IvParameterSpec randomIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }

    /**
     * 伽罗瓦/计数器模式（Galois/Counter Mode）
     * <p>
     * 它结合了计数器模式（CTR）和 Galois 消息认证代码（GMAC）来实现加密和完整性保护‌
     *
     * @param iv recommended 12 bytes for GCM
     * @return GCM
     */
    public static GCMParameterSpec randomGCM(byte[] iv) {
        return new GCMParameterSpec(GCM_TAG_LENGTH, iv);
    }

    /**
     * 伽罗瓦/计数器模式（Galois/Counter Mode）
     * <p>
     * 它结合了计数器模式（CTR）和Galois消息认证代码（GMAC）来实现加密和完整性保护‌。
     * <p>
     * the length of IV recommended 12 bytes.
     * the length of GCM must be one of {128, 120, 112, 104, 96} bits.
     *
     * @return GCM
     */
    public static GCMParameterSpec randomGCM() {
        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);
        return new GCMParameterSpec(GCM_TAG_LENGTH, iv);
    }

    /**
     * 生成密钥
     *
     * @param key 密钥，长度限定 16、24、32
     * @return SecretKey
     * @throws IllegalArgumentException -
     */
    public static SecretKey genKey(byte[] key) {
        int len = key.length;
        if (len != 16 && len != 24 && len != 32) {
            throw new IllegalArgumentException("secret-key size must be a multiple of 32");
        }
        return new SecretKeySpec(key, CIPHER_AES);
    }

    /**
     * 通过密码和盐值生成密钥
     *
     * @param pwd  密码
     * @param salt 盐值
     * @return -
     * @throws NoSuchAlgorithmException -
     * @throws InvalidKeySpecException  -
     */
    public static SecretKey genKey(String pwd, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2);
        KeySpec spec = new PBEKeySpec(pwd.toCharArray(), salt.getBytes(StandardCharsets.UTF_8),
                ITERATION_COUNT, KEY_LENGTH);
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), CIPHER_AES);
    }

    /**
     * 生成随机密钥
     * <p>
     * 注意：密钥长度的单位是位，128 位的密钥，转换成字节数组，就是 16 字节。
     *
     * @param len 密钥长度（位），可以是 128, 192或 256 位。
     * @return SecretKey
     * @throws IllegalArgumentException -
     */
    public static SecretKey randomKey(int len) throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(CIPHER_AES);
        keyGenerator.init(len, new SecureRandom());
        return keyGenerator.generateKey();
    }

    /**
     * CBC 模式加密
     *
     * @param data 数据
     * @param key  密钥
     * @param iv   加密向量
     * @return 加密后的字符串
     * @throws GeneralSecurityException any exception
     */
    public static byte[] encryptCBC(byte[] data, Key key, IvParameterSpec iv) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance(CIPHER_AES_CBC_PKCS5Padding);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }

    /**
     * CBC 模式解密
     *
     * @param data 数据
     * @param key  密钥
     * @param iv   加密向量
     * @return 加密后的字符串
     * @throws GeneralSecurityException any exception
     */
    public static byte[] decryptCBC(byte[] data, Key key, IvParameterSpec iv) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance(CIPHER_AES_CBC_PKCS5Padding);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }


    /**
     * CBC 模式加密
     *
     * @param data 数据
     * @param key  密钥
     * @param iv   加密向量
     * @return 加密后的字符串
     * @throws GeneralSecurityException any exception
     */
    public static byte[] encryptGCM(byte[] data, Key key, GCMParameterSpec iv) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance(CIPHER_AES_GCM_NoPadding);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }

    /**
     * CBC 模式解密
     *
     * @param data 数据
     * @param key  密钥
     * @param iv   加密向量
     * @return 加密后的字符串
     * @throws GeneralSecurityException any exception
     */
    public static byte[] decryptGCM(byte[] data, Key key, GCMParameterSpec iv) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance(CIPHER_AES_GCM_NoPadding);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }
}
















